Hardening: close the residues recorded through layer 8 - #184
Merged
Conversation
… uv.toml The ambient half (#179): child_env() drops every UV_* variable outside a closed plumbing allowlist — cache location, timeouts, TLS, air-gap mode, index credentials, uv's own recursion guard — so an exported UV_NO_BINARY or UV_PYTHON can no longer steer what a sync installs while env_version reports nothing moved. The run verbs (materialize, lc run) name any non-empty variable the scrub dropped, from the same predicate, so the report cannot disagree with the scrub. The config-file half (#176, the advisory option): uv merges user- and system-level uv.toml underneath the project's settings, and list settings concatenate across levels — so the scan now checks the two documented paths per platform for audited install-settings keys and reports a hit beside sdist_built. Reported, never hashed: machine state in env_version would make one commit answer differently on two hosts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
…the OCI wrap The containerized backend was the one mechanism restricting the network, which made "containerized" carry a promise the direct mechanisms never made — and the direction of the recorded decision is symmetry: nothing pretends to a control it does not apply. The wrap now emits no --network flag, the attestation says `allowed` like landlock and seatbelt, and the `denied` literal stays reserved for a mechanism that genuinely emits a denial flag. This also retires the Perlmutter spike item about `--network none` hanging on compute nodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
Closes the cross-write residue: a concurrent task landing bytes in a sibling's output directory before the sibling hashed produced a manifest that was self-consistent and wrong — undetectable by any checksum, so prevention is the only fix. exec_policy gains one keyword (output_dir), handed down from the worker's task; a probe has no output id and keeps results/ whole, which is now the one probe/recipe asymmetry: the probe→recipe promise excludes exactly the commands that write outside their own output directory. All three mechanisms express the narrower shape natively (the same nested-writable-directory form, one level deeper), and the OCI mount table inherits it from the write set with no backend change. Integrity-answering stays data_version's job alone. The enforcement test is mutation-checked: the same cross-write through Unavailable() succeeds (verified rc 0, bytes replaced). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The engine-closure decision's concrete loss was that nothing recorded which uv converged the environment — the one tool between the lock and the installed artifacts. The driver probes `uv --version` once per run and hands it down (the HEAD discipline; the rerun entry point probes its own), and the worker records it beside lc_version. Attestation, never identity: defaulted empty, outside both hashes, never read by classify, and an unparseable probe records "" rather than failing a run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
… as sha256 Every file in an output directory now appears in the crate as a File under its dataset's hasPart, carrying sha256 and contentSize parsed from its SHA256E annex key — the raw digest `sha256sum` can verify after a `git archive` deposit, available with none of the bytes fetched because keys are repository state (dataset.annex_keys; `--include=*` is load-bearing — bare `find` lists only present files). A non-SHA-256 backend key yields size and no digest, never a wrong one; git-carried files (the lock, universes, manifests) hash their own bytes. The key map is injected into render like the writer, so the builder stays git-free and the render pure. This also fixes a latent honesty bug: an out-of-tree declared input's recorded input_versions digest is lc's *framed* hash, and publishing it under the workflow-run `sha256` term claimed a checksum nothing could verify. Externals now publish no digest — the manifests keep the full story, the layer's stated weaker promise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The rerun-lag residue made visible. The render pins datePublished to the newest manifest finished_at, so status can read lag off the document itself — a content comparison against the manifests the walk already read, with no git call and no rocrate import (the crate stays the one materialize-only dependency on status's path; license_of and CRATE_FILENAME move to project.py for the same reason). Four states: not maintained / will be created / up to date / behind, plus an honest "unreadable" for a corrupted file — and the line lands in --json through as_dict like the rest of the header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The mid-run edit hole, surfaced: the dirty check runs at start of run while manifests are written per-output later, so an edit in between left manifests whose git_sha no longer described the code that ran, silently. The run now ends with one dataset.status call — the tree started clean and save/restore keeps results/ clean, so any dirt appeared mid-run — and warns with the edited paths. Still a warning, never a manifest field: the spec's git_dirty stays unwritten by decision. Also closes the recorded review item: a metadata test now pins that no [project.scripts] entry ever targets lightcone.engine or the sandbox shim — every entry point is the CLI or a mirrored git-annex executable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The uv identity holes (scrub landed, machine-config advisory), the crate's raw-digest rule and the injected key map, the status crate line, and the launcher-scrub note resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The sweep: dataset.save now commits with the same pathspec it stages —
a partial commit, built from HEAD plus its own paths alone — so work the
user staged while a graph ran stays staged and warned-about, never swept
into an output or crate commit. The nothing-to-commit probe is scoped
the same way, and the annex per-add config rides on the commit too,
since a partial commit takes the paths through the clean filter again.
The stronger move — a frozen execution worktree — is recorded as
deferred to the venue era, not rejected.
The crate's byte fallback re-checks the annex pointer shape before
hashing: annex_keys answers empty for the whole repository when
git-annex cannot answer at all, and a pointer file reads perfectly well,
so one failed `git annex find` would otherwise publish a well-formed
digest of the pointer text for every output file. The key parse splits
from the last tab (git-annex emits ${file} unescaped; keys never contain
tabs), and _file stops hashing the license file twice.
The scrub allowlist keeps four more plumbing variables —
UV_PYTHON_INSTALL_DIR (the interpreter store has no project-level
spelling, so scrubbing it shipped a remedy that does not exist),
UV_LINK_MODE, UV_PYTHON_INSTALL_MIRROR, UV_KEYRING_PROVIDER — and the
warning is composed once (project.uv_scrub_warning) and surfaced by
every uv-acting verb: init and build now say it too, instead of leaving
a corporate-mirror user with uv's raw resolution error. The machine
config probe covers XDG_CONFIG_DIRS, keeping its "complete" claim true.
Honesty edges: the crate status line says "up to date with the outputs"
/ "behind the outputs" — the exact scope of its datePublished proxy,
correct in both directions (a dropped output regresses the newest stamp
as a rerun advances it); the write-denial remedy names the recipe's own
output directory instead of results/; the Landlock/Seatbelt divergence
the narrowing exposed (`rm -rf $OUT` is EACCES on Linux, fine on macOS)
is recorded with its fix — delete the redundant prelude, the worker
resets the directory; the deposit-vs-consumed meaning of a File's sha256
is recorded where the old conflict rule used to be; and a comment that
referenced the spec stands on its own now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
Reuse: the annex pointer-shape test is assets.is_pointer now — one spelling of git-annex's isPointerFile rule, used by require_fetched and the crate's byte fallback alike, ending the cross-module reach into private constants. dataset gains _ask, the one seam for git reads where "cannot say" must be an answer (last_writer and annex_keys shared the shape verbatim). Altitude: the uv-scrub warning has one composer and only engine surfaces — convergence's report, materialize's warnings, and the probe's outcome notes — so the CLI never composes engine facts and lc run's --json-less echo asymmetry is gone. The lc build echo is deleted outright: the image's uv runs inside the container build, where the host environment does not reach. The driver-resolved run facts (env_version, HEAD, the versions memo, runtime, uv_version) are one frozen RunContext handed to every task, so the next attestation field is one line instead of an edit to five signatures. Simplification: scrubbed_uv_vars folded into uv_scrub_warning (its only caller); the test-only defaults on execute's uv_version and LockScan.machine_config are gone — a real caller can no longer forget either silently. Efficiency: _dataset selects an output's files by bisecting one sorted key list instead of re-sorting and rescanning the whole map per output, and _integrity reads a file once. Tests: one _git_calls helper in test_container, one _cluster helper for the custom-scheduler tests in test_materialize. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
Two host facts the runners exposed: CI pins its matrix interpreter through an ambient UV_PYTHON, which the scrub correctly drops and reports — so every converge grew the warning and warnings == [] depended on the host; an autouse fixture now strips scrubbable UV_* suite-wide, derived from the scrub's own predicate. And the annex_keys clone test ran `git annex init` in a clone with no git identity — a clone inherits no local config, and init in a clone has remote git-annex branch state to commit, where a fresh repo's init tolerates the absence; the test sets identity the way the repo fixture does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
Widens #183's label gate: the eval runs on every non-draft PR, with `ready_for_review` beside the default types so flipping a draft to ready triggers the run the draft guard skips. The per-PR concurrency group already cancels superseded runs, so a push train costs one eval. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
Contributor
✅ Eval
lc statusConfusion & pain points (Claude analysis)Confusion & pain points
Full trace: |
EiffL
added a commit
that referenced
this pull request
Aug 21, 2026
…ywhere PR #184 dropped the OCI wrap's --network none so all three mechanisms attest network: allowed symmetrically; the container internals page still said recipes exec network-denied. Found by the drift check the new check-docs prompt describes — a quoted flag the code no longer delivers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx
This was referenced Aug 21, 2026
EiffL
added a commit
that referenced
this pull request
Aug 21, 2026
Follow-up to the PR #190 eval trace, which surfaced three harness-attributable frictions (the other findings were agent-side noise). ## The `lc run` convention question, settled Checked the ecosystem before touching anything: `uv run "python -V"` fails with ``Failed to spawn: `python -V` `` (verified live), `docker run img "echo hi"` fails with `exec: "echo hi": executable file not found` — argv with no shell parsing is the convention across `docker run`, `uv run`, `kubectl exec`, `pipx run`, `poetry run`, `srun`. **`lc run` already matches it**, so the behavior stands unchanged and the syntax gets one line in the two places it was missing: - `evals/prompt.md`: "Argv style, like `docker run` or `uv run`: `lc run python scripts/fit.py --output /tmp/x`, never a single quoted shell string; for shell syntax use `lc run bash -c '...'`." - `docs/cli/run.md`: same sentence, in the synopsis. In the trace, the missing line cost one round trip through a genuinely cryptic failure (`/usr/bin/env: '<whole string>': No such file or directory` + the sandbox trailer insinuating a denial). ## Harness environment - **`UV_PYTHON` noise**: `setup-uv`'s `python-version` input exports `UV_PYTHON`, and the #184 install-settings scrub then (correctly) warned `ignored ambient UV_PYTHON — …` on *every* `lc run`/`lc materialize` — a dozen-plus unactionable lines the agent ended up `grep -v`'ing to read its own output. The input is dropped; determinism is kept by pinning `--python 3.12` on both tool installs. Same category as the `VIRTUAL_ENV` cleanup earlier. - **Truthful dev versions**: the shallow checkout had no tags, so hatch-vcs stamped `0.1.dev1+g<sha>` into the installed engine — and into every manifest's `lc_version`. `fetch-depth: 0` fixes it. Validation: label this PR `run-eval` to see the updated prompt and clean environment in action. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
EiffL
added a commit
that referenced
this pull request
Aug 21, 2026
…ibility (#190) This merges the clean rebuild of lightcone-cli onto main — the full re-architecture from the Snakemake shim to the model where **the locked environment is the execution environment**. The pre-rebuild codebase was deliberately stripped and functionality re-added one layer at a time, each layer landing with its own tests, dependencies, and (since the rewrite) docs. 21 PRs, all individually reviewed and merged into `clean_rebuild`; this PR is the promotion. ## What lightcone-cli is now A project is `pyproject.toml` + `uv.lock` + `astra.yaml` — uv is the only environment substrate, and mode is derived, never configured: direct by default (recipes in the project's `.venv` under Landlock/Seatbelt), containerized when `[tool.lightcone.image]` is declared (recipes in a content-addressed image archived in the repository itself). Five verbs: `init`, `materialize`, `status`, `run`, `build`. - **Identity**: `definition_version` (what an output *is*) and `env_version` (what it ran under) are separate on purpose — a spec contradiction is `stale` and remade; an environment move is `behind`, reported and left alone. `--refresh` widens by exactly that one state. - **Storage**: the DataLad model — git carries history, git-annex carries bytes, behind ordinary `git add`/`git commit`. Every output is committed with a manifest and a run record `datalad rerun` can replay; runs start and end on a clean tree; foreign writes are detected by history and classify stale. - **Execution**: one exec boundary (`Policy` → pure argv rewrite → honest attestation) shared by recipes and the `lc run` probe; a recipe writes its own output directory and nothing else; denials carry copy-pasteable remedies. Network is uncontrolled on every mechanism, symmetrically attested. - **Venues**: detected, never configured — a SLURM allocation is spanned with one worker per node; known centers get a login guard whose refusal prints the center's own `salloc`/`sbatch` spellings; podman-hpc makes multi-node containerized runs real. - **Publication**: declaring `[project].license` turns on the RO-Crate view — `ro-crate-metadata.json` converged by materialize, validated against Provenance Run Crate 0.5 in CI. Deposit is `git archive`. - **The engine is the host's uv tool, never a project dependency** — a project's lock carries only what the analysis imports, and an engine upgrade rebuilds nothing. ## The layers, as merged | PR | Layer | |---|---| | #173 | Layer 5: the exec boundary and `lc run` (landed first — the smallest honest consumer of the boundary) | | #175 | Layers 2 + 4: identity, provenance, `lc materialize` | | #178 | The engine is the host's uv tool (deletes layer 3 by decision) | | #180 | Layer 6: the container hatch | | #181 | Layer 7: venues — SLURM in-allocation, podman-hpc | | #182 | Layer 8: the publication view | | #184 | Hardening: the recorded residues closed (UV_* scrub, write-scope narrowing, symmetric network attestation, machine-level uv.toml advisory) | | #183 | The agentic eval reactivated: `uv tool install` of the branch under test, empty-environment task, RO-Crate gate — three green runs | | #185–#189 | The documentation rewritten end to end: README, user guide, CLI reference, developer corner, governance — every command block executed before shipping, every refusal quoted from a real run | ## Verification - The suite (tests, lint, mypy strict) is green on `clean_rebuild`, including the three gated suites CI requires: sandbox enforcement (Landlock on Linux, Seatbelt on macOS), container smoke (real build → archive → bytes-free-clone `datalad rerun`), and crate smoke (the official validator, REQUIRED clean). - The agentic eval passes end-to-end on this branch: an agent builds a supernova-cosmology analysis from the spec, manages the environment with `uv add`, materializes through the real engine, and publishes a crate — $0.70–$1.22 per run. - `zensical build` renders the rewritten docs with zero issues; the site deploys on the next release. ## Known deferred work (tracked, not blocking) - The one-time **Perlmutter spike** (layer 7's pending items are listed in CLAUDE.md) — HPC support ships honestly labeled "early days". - **hub/GKE + Cloud Build** venue and **apptainer/singularity** runtimes — design headroom recorded, deliberately not stubbed. - Multi-arch image archives; the design-doc drop and CLAUDE.md slimming when the rebuild formally closes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DJzmp2MUhwiNHR94cB91dx --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The rebuild is functionally complete through layer 8; this PR closes the
recorded residues that do not need Perlmutter access, one commit per
decision. Each choice below was settled explicitly before implementation.
What lands
UV_*scrub (closes Scrub the ambient UV_* namespace before invoking uv #179):project.child_envdrops everyUV_*variable outside a closed plumbing allowlist (cache dir,timeouts, TLS, air-gap mode, index credentials, uv's recursion guard),
so an exported
UV_NO_BINARYorUV_PYTHONcan no longer steer a syncwhile
env_versionreports nothing moved. The run verbs warn with thenames of any non-empty variable dropped, from the same predicate.
uv's two documented user/system config paths for audited
install-settings keys — key presence, not file presence, because list
settings concatenate across levels — and reports a hit beside
sdist_built. Never hashed.--network none, its one restriction, so all three mechanisms attestnetwork: allowedsymmetrically and no consumer can read a promiseinto "containerized".
deniedstays in the type for a mechanism thatgenuinely emits a denial flag. Retires the Perlmutter spike item about
--network nonehanging on compute nodes.the recorded "not narrowed" decision): closes the cross-write residue —
a concurrent task landing bytes in a sibling's directory before the
sibling hashed produced a manifest that was self-consistent and wrong,
undetectable by any checksum, so prevention is the only fix. A probe
keeps
results/whole (no output id) — the one probe/recipe asymmetry,and it excludes exactly the accident being prevented. The new
enforcement test is mutation-checked through
Unavailable().uv_versionmanifest attestation: the engine-closure decision'sconcrete loss, recorded — probed once per run by the driver and handed
down (the HEAD discipline), beside
lc_version. Attestation, neveridentity.
a
Fileunder its dataset'shasPartwithsha256/contentSizeparsed from its SHA256E key — verifiable by
sha256sumafter agit archivedeposit, available bytes-free because keys are repositorystate (
--include=*is load-bearing). Non-SHA-256 keys yield size andno digest; git-carried files hash their bytes. Also fixes a latent
honesty bug: out-of-tree inputs published lc's framed digest under
the workflow-run
sha256term; they now publish none. Validator floorunchanged at five.
crate:line inlc status: the rerun-lag residue made visible bycomparing the document's own
datePublishedagainst the newestmanifest
finished_atthe walk already read — no git, no rocrateimport on status's path.
git_dirtywindow) is now named in the report; still no manifestfield, by decision.
[project.scripts]absence pinned: the recorded review item — ametadata test asserts the worker and the shim never become console
scripts.
Re-examined and deliberately left (recorded in CLAUDE.md)
adversaries, and a copied subject is already deliberate.
publication-metadata config surface.
Housekeeping
Closed stale pre-rebuild issues #166, #128, #153 with pointers;
commented triage on #152 and the advisory landing on #176.
Verification
Full suite: 582 passed (sandbox enforcement real on Linux; crate smoke
with the official rocrate-validator, REQUIRED clean and the floor
unchanged). ruff and mypy clean. Cross-write denial mutation-checked
through
Unavailable()(rc 0, bytes replaced, unsandboxed).Closes #179.
🤖 Generated with Claude Code
https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa